﻿using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using SkiaSharp;
using System.Security.Cryptography;
using System.Text;

namespace PmSoft.Web.Abstractions.Captcha;
public class CaptchaService
{
    private readonly IDistributedCache _cache;
    private readonly CaptchaOptions _options;
    private const string CACHE_PREFIX = "captcha:"; // 缓存键前缀

    /// <summary>
    /// 随机数：随机种子----Ticks是 long 类型，强制到 int 类型可能报错~unchecked避免报错
    /// </summary>
    internal static Random RandomNumber = new(~unchecked((int)DateTime.Now.Ticks));
    /// <summary>  
    /// 预设定颜色  
    /// </summary>  
    private static readonly SKColor[] SkColors = new SKColor[]
    {
        SKColors.DeepSkyBlue,
        SKColors.DodgerBlue,
        SKColors.CornflowerBlue,
        SKColors.Aqua,
        SKColors.Aquamarine,
        SKColors.Cyan,
        SKColors.LightGreen,
        SKColors.SteelBlue,
        SKColors.DodgerBlue,
        SKColors.Black,
        SKColors.Red,
        SKColors.DarkBlue,
        SKColors.Green,
        SKColors.Orange,
        SKColors.Brown,
        SKColors.BlueViolet,
        SKColors.Turquoise,
        SKColors.CadetBlue,
        SKColors.Violet,
        SKColors.Tomato,
        SKColors.Tan,
        SKColors.MediumSeaGreen
    };

    /// <summary>
    /// 构造函数，注入分布式缓存和配置选项
    /// </summary>
    /// <param name="cache">分布式缓存服务实例</param>
    /// <param name="options">验证码配置选项</param>
    public CaptchaService(IDistributedCache cache, IOptions<CaptchaOptions> options)
    {
        _cache = cache;
        _options = options.Value;
    }

    /// <summary>
    /// 验证验证码是否正确
    /// </summary>
    /// <param name="code">用户输入的验证码字符串</param>
    /// <param name="id">验证码的唯一标识符</param>
    /// <returns>验证结果，true表示正确，false表示错误</returns>
    public async Task<bool> CheckAsync(string code, string id)
    {
        if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(id))
            return false;

        string key = CACHE_PREFIX + GetAuthCode(_options.SeKey, id);
        byte[]? cachedData = await _cache.GetAsync(key);

        if (cachedData == null)
            return false;

        string cachedCode = Encoding.UTF8.GetString(cachedData);
        if (code.ToUpper() == cachedCode)
        {
            if (_options.Reset)
                await _cache.RemoveAsync(key);
            return true;
        }
        return false;
    }

    /// <summary>
    /// 创建非图形验证码
    /// </summary>
    /// <param name="id">验证码的唯一标识符</param>
    /// <param name="captcha">指定的验证码字符串，可选，若不提供则随机生成</param>
    /// <returns>生成的验证码字符串</returns>
    public async Task<string> CreateAsync(string id, string? captcha = null)
    {
        string key = CACHE_PREFIX + GetAuthCode(_options.SeKey, id);
        string code = GenerateCode(captcha);

        var cacheOptions = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_options.ExpireSeconds)
        };

        await _cache.SetAsync(key, Encoding.UTF8.GetBytes(code), cacheOptions); // 存储原始验证码
        return code;
    }

    /// <summary>
    /// 生成验证码图片
    /// </summary>
    /// <param name="id">验证码的唯一标识符</param>
    /// <returns>验证码图片的字节数组</returns>
    public async Task<byte[]> GenerateImageAsync(string id)
    {
        int width = _options.ImageWidth;
        int height = _options.ImageHeight;

        var info = new SKImageInfo(width, height);
        using var surface = SKSurface.Create(info);
        var canvas = surface.Canvas;

        canvas.Clear(new SKColor((byte)_options.BackgroundColor[0],
                               (byte)_options.BackgroundColor[1],
                               (byte)_options.BackgroundColor[2]));

        string key = CACHE_PREFIX + GetAuthCode(_options.SeKey, id);
        byte[]? cachedData = await _cache.GetAsync(key);
        string code;

        if (cachedData != null)
        {
            code = Encoding.UTF8.GetString(cachedData); // 直接获取原始验证码
        }
        else
        {
            code = GenerateCode();
            await _cache.SetAsync(key, Encoding.UTF8.GetBytes(code), // 存储原始验证码
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_options.ExpireSeconds)
                });
        }

        if (_options.UseNoise)
            DrawNoisePoints(canvas, _options.Length * 50, width, height);

        if (_options.UseCurve)
            DrawNoiseLines(canvas, _options.Length, width, height);

        // 绘制波纹线
        if (_options.UseWavy)
        {
            DrawWavyLines(canvas, _options.Length - 1, width, height);
        }

        DrawText(canvas, code, width, height);

        using var image = surface.Snapshot();
        using var data = image.Encode(SKEncodedImageFormat.Png, 100);
        return data.ToArray();
    }

    /// <summary>
    /// 生成随机验证码字符串
    /// </summary>
    /// <param name="captcha">指定的验证码字符串，可选，若不提供则随机生成</param>
    /// <returns>生成的验证码字符串</returns>
    private string GenerateCode(string? captcha = null)
    {
        if (!string.IsNullOrEmpty(captcha))
            return captcha;

        var random = new Random();
        var code = new StringBuilder();
        for (int i = 0; i < _options.Length; i++)
        {
            code.Append(_options.CodeSet[random.Next(_options.CodeSet.Length)]);
        }
        return code.ToString().ToUpper();
    }

    /// <summary>
    /// 生成验证码加密字符串
    /// </summary>
    /// <param name="str">需要加密的输入字符串</param>
    /// <param name="id">验证码的唯一标识符</param>
    /// <returns>加密后的字符串</returns>
    private string GetAuthCode(string str, string id)
    {
        var key = MD5.HashData(Encoding.UTF8.GetBytes(_options.SeKey)).Skip(5).Take(8).ToArray();
        var strHash = MD5.HashData(Encoding.UTF8.GetBytes(str)).Skip(8).Take(10).ToArray();
        return Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(
            Convert.ToHexString(key) + Convert.ToHexString(strHash) + id)));
    }

    /// <summary>
    /// 添加干扰曲线
    /// </summary>
    /// <param name="canvas">SkiaSharp画布对象</param>
    /// <param name="width">图片宽度</param>
    /// <param name="height">图片高度</param>
    private static void DrawNoiseLines(SKCanvas canvas, int count, int width, int height)
    {
        for (var i = 0; i < count; i++)
        {
            var x1 = RandomNumber.Next(width);
            var y1 = RandomNumber.Next(height);
            var x2 = RandomNumber.Next(width);
            var y2 = RandomNumber.Next(height);
            var color = SkColors[RandomNumber.Next(SkColors.Length)];

            using var paint = new SKPaint
            {
                IsAntialias = true,
                Style = SKPaintStyle.Stroke,
                StrokeWidth = 1,
                Color = color
            };
            canvas.DrawLine(x1, y1, x2, y2, paint);
        }
    }
    /// <summary>
    /// 添加干扰点
    /// </summary>
    /// <param name="canvas"></param>
    /// <param name="count"></param>
    private static void DrawNoisePoints(SKCanvas canvas, int count,int width,int height)
    {
        for (var i = 0; i < count; i++)
        {
            var x = RandomNumber.Next(0, width);  //随机位置  
            var y = RandomNumber.Next(0, height);
            var color = SkColors[RandomNumber.Next(SkColors.Length)];
            canvas.DrawPoint(x, y, new SKPaint { Color = color, IsAntialias = true });
        }
    }
    /// <summary>
    /// 绘制随机干扰线
    /// </summary>
    /// <param name="canvas"></param>
    /// <param name="count"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    private static void DrawWavyLines(SKCanvas canvas, int count, int width, int height)
    {
        for (var i = 0; i < count; i++)
        {
            using var path = new SKPath();
            path.MoveTo(new SKPoint(RandomNumber.Next(width), RandomNumber.Next(height)));

            for (int j = 0; j < 4; j++)
            {
                path.LineTo(new SKPoint(RandomNumber.Next(width), RandomNumber.Next(height)));
            }

            var color = SkColors[RandomNumber.Next(SkColors.Length)];
            using var paint = new SKPaint
            {
                IsAntialias = true,
                Style = SKPaintStyle.Stroke,
                StrokeWidth = 1,
                Color = color
            };
            canvas.DrawPath(path, paint);
        }
    }
    private void DrawText(SKCanvas canvas, string code, int width, int height)
    {
        float letterSpacing = 1f;
        int fontSize = _options.FontSize;
        float totalTextWidth = 0f;
        foreach (var charCode in code)
        {
            using var skFont = new SKFont
            {
                Typeface = SKTypeface.FromFamilyName("Arial"),
                Size = fontSize,
                Embolden = true,
            };
            totalTextWidth += skFont.MeasureText(charCode.ToString()) + letterSpacing;
        }
        // 计算 drawX 的初始值，靠左显示
        var drawX = (width - totalTextWidth) / _options.Length;
        // 计算 drawY 的初始值，使文本垂直居中
        var drawY = height / 2f;

        foreach (var charCode in code)
        {
            var color = SkColors[RandomNumber.Next(SkColors.Length)];
            using var paint = new SKPaint
            {
                Color = color,
                Style = SKPaintStyle.Fill,
                // 启用抗锯齿，使绘制的字符边缘更平滑
                IsAntialias = true,
            };

            using var skFont = new SKFont
            {
                Typeface = SKTypeface.FromFamilyName("Arial"),
                Size = fontSize,
                // 设置字体加粗
                Embolden = true,
            };

            // 计算当前字符的宽度，并加上字符间距
            var charWidth = skFont.MeasureText(charCode.ToString()) + letterSpacing;

            // 判断是否使用渐变效果
            if (_options.UseGradient)
            {
                // 从 SkColors 数组中再随机选择一种颜色，用于渐变效果
                var color2 = SkColors[RandomNumber.Next(SkColors.Length)];
                // 定义渐变的颜色数组，包含起始颜色和结束颜色
                SKColor[] colors = new SKColor[] { color, color2 };
                // 创建一个线性渐变的着色器
                paint.Shader = SKShader.CreateLinearGradient(
                    new SKPoint(drawX, 0),
                    new SKPoint(drawX + charWidth, 40),
                             colors,
                    SKShaderTileMode.Clamp);
            }
            
            canvas.Translate(RandomNumber.Next((int)drawX / _options.Length), RandomNumber.Next((int)drawY / _options.Length));
            canvas.DrawText(charCode.ToString(), drawX, drawY, skFont, paint);
            drawX += charWidth;
        }
    }
}
